package com.subpackage

import com.Crc32
import com.CustomScript
import com.entity.PackPath
import com.entity.toPackPath
import com.entity.yaml.AppConfig
import com.entity.yaml.Project
import com.utils.PropertiesUtils
import com.utils.ScriptUtils
import com.utils.YamlUtil
import extend.error
import extend.printLog
import kotlinx.coroutines.*
import org.apache.logging.log4j.kotlin.logger
import java.util.*
import java.util.concurrent.ConcurrentLinkedDeque

/**
 * 对比文件
 */
class ContrastFile : BasePack() {


    /**
     * 存储扫描到的所有文件路径列表
     *
     * 保存的是输入路径(input)下的所有文件，这些是源文件路径
     * 用于与缓存中的文件进行对比，检测文件变化情况
     */
    lateinit var scanAllPath: List<PackPath>

    /**
     * 存储需要更新的文件路径集合
     *
     * 保存的是输出路径(out)下的文件路径，即复制到目标目录后的文件路径
     * 当检测到文件内容发生变化时，会将更新后的文件保存到config.outDir指定的输出目录中，
     * 并将这些输出文件的路径存储在此集合中
     */
    lateinit var updateFileList: MutableSet<PackPath>

    /**
     * 存储需要删除的文件路径集合
     *
     * 保存的是输出路径(out)下的文件路径，即需要从输出目录中删除的文件路径
     * 当检测到某些文件在输入目录中已不存在但在缓存中存在时，
     * 会将这些文件在输出目录中的对应路径存储在此集合中，用于从输出目录删除文件
     */
    lateinit var deleteFileList: MutableSet<PackPath>

    /**
     * 存储新增文件的路径集合
     *
     * 保存的是输出路径(out)下的文件路径，即新增文件复制到输出目录后的路径
     * 当检测到新文件(在缓存中不存在的文件)时，会将这些文件复制到config.outDir指定的输出目录中，
     * 并将这些输出文件的路径存储在此集合中
     */
    lateinit var addFileList: MutableSet<PackPath>


    /**
     * 启动打包流程的主要入口方法
     *
     * 该方法会读取配置信息，检查项目配置是否正确，
     * 然后调用[startCheck]进行文件对比检查，
     * 最后保存处理结果并输出完成日志。
     */
    fun startPack() {
        val config = YamlUtil.getAppConfig()
        if (config.projects.isEmpty()) {
            printLog("项目配置属性 config.projectDir | config.projects 未配置")
            return
        }

        if (config.global.clearCacheOnStart) {
            printLog("清空缓存目录 ${config.outDir.absolutePathString}")
            config.outDir.deleteRecursively()
        }

        runCatching {
            startCheck(config)

            ScriptUtils.script.runCustomScript(CustomScript::onBeforeClose)

            //统一保存本次处理
            PropertiesUtils.instance.store()

            val clearCache = config.runEnvironment().clearCache || config.global.clearCache
            if (clearCache) {
                logger.info("清空缓存目录 ${config.outDir}")
                config.outDir.deleteRecursively()
            }
            logger.info("全部打包完成")
        }.onFailure {
            logger.error(it.message, it)
        }


    }

    /**
     * 执行文件对比检查的核心方法
     *
     * 此方法负责：
     * 1. 初始化文件列表和属性工具
     * 2. 执行自定义脚本
     * 3. 设置输出目录和缓存路径
     * 4. 创建忽略文件列表
     * 5. 获取所有需要处理的文件
     * 6. 检查删除、新增和修改的文件
     * 7. 调用统计和打包方法
     *
     * @param config 应用配置对象，包含项目路径、输出路径等信息
     */
    private fun startCheck(config: AppConfig) {
        ConcurrentLinkedDeque<String>()
        updateFileList = Collections.synchronizedSet(mutableSetOf())
        addFileList = Collections.synchronizedSet(mutableSetOf())
        deleteFileList = Collections.synchronizedSet(mutableSetOf())

        PropertiesUtils.instance.clear()

        // 执行完成文件整理 检查执行的命令
        ScriptUtils.script.runCustomScript(CustomScript::onBeforeCompleteOnceCheck)

        config.global.uploadParent = config.global.uploadParent ?: config.outDir
        config.outDir.deleteRecursively()
        config.outDir.createDirectories()
        if (config.global.cachePath.pathString.isBlank()) {
            config.global.cachePath = config.outDir.toAbsolutePath()
        }

        logger.info("处理路径=" + config.projects)
        logger.info("输出路径=" + config.outDir)
        logger.info("本地根路径=" + config.global.uploadParent)

        val cache = config.global.cachePath.resolve(config.global.cacheName)
        logger.info("文件变化crc缓存路径 ${cache.absolutePathString}")
        PropertiesUtils.instance.read(cache.toPath)
        // 创建忽略文件列表
        createIgnoreFile(config.ignoreList + config.runEnvironment().ignoreList, "**/" + config.global.cacheName)

        this.scanAllPath = getAllFile(config.projects)
        if (scanAllPath.isEmpty()) {
            printLog("未发现文件！${config.projects}")
            return
        }
        val fileListPath = scanAllPath.map { it.pathString }
        logger.info("新旧文件数量对比1=" + scanAllPath.size + "|" + PropertiesUtils.instance.size())

        runBlocking {
            launch {
                // 协程1：处理删除的文件
                handleDeletedFiles(config, fileListPath)
            }

            launch {
                // 协程2：处理新增或修改的文件
                handleAddedOrModifiedFiles(config, scanAllPath)
            }
        }

        if (addFileList.isEmpty() && deleteFileList.isEmpty() && updateFileList.isEmpty()) {
            printLog("未发现变动文件！")
            return
        }

        ScriptUtils.script.completeOnceCheck?.invoke()
        ScriptUtils.script.runCustomScript(CustomScript::onAfterCompleteOnceCheck)
        // 统计更新情况
        statistics()

        // 存入上传目录
        deleteOssPush.addAll(deleteFileList)
        addOssPush.addAll(addFileList)
        addOssPush.addAll(updateFileList)
        subpackage()
    }


    /**
     * 处理已删除的文件
     */
    private suspend fun handleDeletedFiles(config: AppConfig, fileListPath: List<String>) =
        withContext(Dispatchers.IO) {
            // 检查删除
            val keySet = PropertiesUtils.instance.keySet()
            val tempDelete = mutableListOf<String>()

            for (str in keySet) {
                val key = getRelativePath(str)
                if (key != null) {
                    val isExists = fileListPath.contains(str)
                    // 如果不存在
                    if (!isExists) {
                        logger.info("此文件已经不存在! $key")
                        tempDelete.add(key)
                        val deleteFile = config.outDir.resolve(key)
                        deleteFileList.add(deleteFile)
                        if (deleteFile.exists()) {
                            logger.info("删除缓存文件! " + deleteFile.pathString)
                            deleteFile.deleteRecursively()
                        }
                    }
                }
            }

            for (i in tempDelete.indices) {
                deleteCacheContainsKey(tempDelete[i]) // 删除记录
            }
        }

    /**
     * 处理新增或修改的文件
     */
    private suspend fun handleAddedOrModifiedFiles(config: AppConfig, fileList: List<PackPath>) =
        withContext(Dispatchers.IO) {
            // 检查新增 和 修改
            fileList.map { file ->
                async(Dispatchers.IO) {
                    val key = getRelativePath(file, false)!!
                    val fileCrc = getFileCrc32(file)
                    if (checkCacheContainsKey(key)) {
                        val cacheCrc = getCacheContainsCrc2(key)
                        if (cacheCrc != fileCrc) { // 不一样  内容有变动
                            logger.info("更新文件 = $key")
                            val newFile = config.outDir.resolve(key)
                            newFile.createParentDirectories()
                            file.copyTo(newFile, true)
                            PropertiesUtils.instance.add(file.absolutePathString, fileCrc)
                            updateFileList.add(newFile)
                        }
                    } else {
                        logger.debug("新增加文件 = $key")
                        val saveFile = config.outDir.resolve(key)
                        saveFile.createParentDirectories()
//                        println("${file.name} ${savaFile.name}  ${file.isReadable()}  ||   ${file.isWritable()}")
                        file.copyTo(saveFile, true)
                        PropertiesUtils.instance.add(file.absolutePathString, fileCrc)
                        addFileList.add(saveFile)
                    }
                }
            }.awaitAll()
        }

    /**
     * 删除缓存中与指定相对路径匹配的条目
     *
     * 该方法会在属性缓存中查找与给定相对路径匹配的条目，
     * 如果找到则将其删除。
     *
     * @param key 使用[getRelativePath]处理后的相对路径 未保留Prefix
     * @return 如果成功删除返回true，否则返回false
     */
    private fun deleteCacheContainsKey(key: String): Boolean {
        return key.let {
            PropertiesUtils.instance.keySet()
                .any { str ->
                    (getRelativePath(str) == it).also { b ->
                        if (b) PropertiesUtils.instance.remove(str)
                    }
                }
        }
    }

    /**
     * 检查缓存中是否包含与指定相对路径匹配的条目
     *
     * 该方法用于判断给定的相对路径是否已经在缓存中存在，
     * 不会修改缓存内容。
     *
     * @param key 使用用[getRelativePath]处理后文件相对路径 未保留Prefix
     * @return 如果存在返回true，否则返回false
     */
    private fun checkCacheContainsKey(key: String?): Boolean {
        if (key.isNullOrBlank()) return false
        return key.let {
            PropertiesUtils.instance.keySet()
                .any { str ->
                    getRelativePath(str) == it
                }
        }
    }

    /**
     * 获取缓存中与指定相对路径对应的CRC32值
     *
     * 根据给定的相对路径，在属性缓存中查找匹配的条目，
     * 并返回其存储的CRC32校验值。
     *
     * @param key 用[getRelativePath]处理后的相对路径 未保留Prefix
     * @return 对应的CRC32值，如果未找到返回null
     */
    private fun getCacheContainsCrc2(key: String?): String? {
        if (key.isNullOrBlank()) return null
        return key.let {
            PropertiesUtils.instance.keySet().find { str ->
                getRelativePath(str) == it
            }?.run {
                PropertiesUtils.instance.get(this)
            }
        }
    }

    /**
     * 统计文件变更情况并输出到控制台
     *
     * 该方法会统计新增、删除和更新的文件数量，
     * 并以格式化的方式输出统计结果。
     */
    private fun statistics() {
        printLog("********************  开始统计  ******************")
        printLog("仓库删除文件=" + deleteFileList.size, "详细文件=[${deleteFileList.joinToString()}]")
        printLog("仓库添加文件=" + addFileList.size, "详细文件=[${addFileList.joinToString()}]")
        printLog("仓库更新文件=" + updateFileList.size, "详细文件=[${updateFileList.joinToString()}]")
        printLog("********************  统计完成  ******************")
    }

    /**
     * 打包处理方法
     *
     * 负责执行以下操作：
     * 1. 运行打包前后的自定义脚本
     * 2. 根据配置准备上传数据
     * 3. 执行上传脚本（如果配置了上传）
     * 4. 根据配置清理缓存目录
     */
    private fun subpackage() {
        logger.info("执行自定义打包更新前执行脚本")

        ScriptUtils.script.runCustomScript(CustomScript::onBeforeSubpackageStart)
        ScriptUtils.script.subpackageStart?.invoke()
        ScriptUtils.script.runCustomScript(CustomScript::onAfterSubpackageStart)

        val config = YamlUtil.getAppConfig()

        if (config.runEnvironment().isUpload) {
            logger.info("整理上传资源")
            // 整理上传oss文件
            ossPush = mutableListOf()
            ossPush.add("parent")
            ossPush.add(config.global.uploadParent!!.absolutePathString + "/")
            if (deleteOssPush.isNotEmpty()) {
                ossPush.add("deleteFile")
                ossPush.add(deleteOssPush.joinToString(","))
            }
            if (addOssPush.isNotEmpty()) {
                ossPush.add("addFile")
                ossPush.add(addOssPush.joinToString(","))
            }
        } else {
            logger.info("不上传")
        }
        logger.info("执行自定义打包更新完成后的脚本")

        ScriptUtils.script.runCustomScript(CustomScript::onBeforeSubpackageEnd)
        ScriptUtils.script.subpackageEnd?.invoke()
        ScriptUtils.script.runCustomScript(CustomScript::onAfterSubpackageEnd)

        // 执行上传
        ScriptUtils.script.runCustomScript(CustomScript::onBeforeUpload)
        if (ScriptUtils.script.onUpload != null) {
            if (YamlUtil.getAppConfig().runEnvironment().isUpload) {
                logger.info("执行上传脚本")
                val result = ScriptUtils.script.onUpload!!.invoke()

                if (!result) {
                    error("上传bug")
                }

            } else logger.info("未开启上传功能")
        } else logger.info("未配置上传脚本 ScriptUtils.script.onUpload ")
        ScriptUtils.script.runCustomScript(CustomScript::onAfterUpload)
    }

    /**
     * 根据输入文件路径和配置信息，计算并返回处理后的文件相对路径
     *
     * 该方法用于获取文件相对于输出目录或项目输入目录的相对路径。主要用于：
     * 1. 确定文件在打包结构中的位置
     * 2. 对比缓存中的文件路径
     * 3. 生成上传到OSS的文件路径
     *
     * 示例：
     * ```
     * 假设配置中的 outDir = "/project/output"
     * 文件路径 file = "/project/output/assets/image.png"
     *
     * getRelativePath(file, false) 返回: "assets/image.png"
     * getRelativePath(file, true)  返回: "/assets/image.png"
     *
     * 假设配置中的 projects 包含 inputFile = "/project/src"
     * 文件路径 file = "/project/src/config.json"
     *
     * getRelativePath(file, false) 返回: "config.json"
     * getRelativePath(file, true)  返回: "/config.json"
     *
     * ```
     *
     * @param file 输入的文件绝对路径字符串，可能为null或空
     * @param preservePrefix 布尔标志，用于控制路径截取的起始位置
     *   - true: 保留前缀/
     *   - false: 移除基础路径前缀，只返回相对部分
     *
     * @return 处理后的文件路径字符串，如果无法处理则返回null
     */
    fun getRelativePath(file: String?, preservePrefix: Boolean = false): String? {
        if (file == null) return null
        return getRelativePath(file.toPackPath(), preservePrefix)
    }

    /**
     * @param file 必须是绝对路径
     */
    fun getRelativePath(file: PackPath?, preservePrefix: Boolean = false): String? {
        if (file == null) return null

        val config = YamlUtil.getAppConfig()
        val outDirPath = config.outDir
        var relative: String? = null

        if (file.startsWith(outDirPath)) {
            relative = outDirPath.relativize(file).pathString
        } else {
            for (project in config.projects) {
                val basePath = project.inputPath
                if (basePath != null && basePath.root == file.root) {
                    relative = basePath.relativize(file).pathString
                }
            }
        }
        if (relative != null && preservePrefix) {
            relative = "/$relative"
        }
        return relative
    }

    /**
     * 获取所有非忽略文件的列表
     *
     * 遍历指定路径列表中的所有文件，排除目录和被忽略的文件，
     * 返回符合条件的文件路径列表。
     *
     * @param list 需要遍历的路径列表
     * @return 文件路径列表
     */
    private fun getAllFile(list: List<Project>): List<PackPath> {
        logger.info("被忽略的文件=${ignoreList?.joinToString()}")
        val files = list.mapNotNull {
            it.inputPath
        }
        return files.flatMap { flat ->
            flat.walk()
                .map { it.toPackPath }
                .filter {
                    !it.isDirectory() &&
                            (ignoreList == null ||
                                    ignoreList?.any { ignorePath ->
                                        validatePath(it.pathString, ignorePath)
                                    } == false)
                }
        }
    }


    /**
     * 创建忽略文件列表
     *
     * 合并来自脚本引擎、配置文件和参数中的忽略规则，
     * 构建最终的忽略文件路径集合。
     *
     * @param ignores 来自配置文件的忽略规则集合
     * @param ignore 可变参数，额外的忽略规则
     */
    private fun createIgnoreFile(ignores: Set<String>?, vararg ignore: String) {
        val ignoreList = mutableSetOf<String>()
        ScriptUtils.scriptEngine.getVarValue<Set<String>>("ignore")?.let {
            ignoreList.addAll(it)
        }
        if (ignores != null) ignoreList.addAll(ignores)
        ignoreList.addAll(ignore)
        this.ignoreList = ignoreList
    }

    /**
     * 计算文件的CRC32校验值
     *
     * 使用[Crc32]工具类计算指定文件的CRC32校验和，
     * 并将结果转换为十六进制字符串。
     *
     * @param path 需要计算校验值的文件路径
     * @return 文件的CRC32校验值（十六进制字符串）
     */
    @OptIn(ExperimentalStdlibApi::class)
    fun getFileCrc32(path: PackPath): String {
        // 计算路径对应的CRC32校验和
        val hashValue = Crc32.crc32(path)
        // 将计算得到的整型校验和转换为十六进制字符串
        val hash = Integer.toHexString(hashValue.toInt())
        return hash
    }

}